چندریسمانی واقعی را در جاوااسکریپت فعال کنید. این راهنمای جامع SharedArrayBuffer، Atomics، Web Workers و الزامات امنیتی برای برنامههای وب با کارایی بالا را پوشش میدهد.
SharedArrayBuffer در جاوااسکریپت: نگاهی عمیق به برنامهنویسی همزمان در وب
برای دههها، ماهیت تکریسمانی (single-threaded) جاوااسکریپت هم منبع سادگی آن و هم یک گلوگاه عملکردی مهم بوده است. مدل حلقه رویداد (event loop) برای اکثر وظایف مبتنی بر رابط کاربری به زیبایی کار میکند، اما زمانی که با عملیات محاسباتی سنگین مواجه میشود، دچار مشکل میشود. محاسبات طولانیمدت میتوانند مرورگر را قفل کرده و تجربه کاربری ناخوشایندی ایجاد کنند. در حالی که Web Workers با اجازه دادن به اجرای اسکریپتها در پسزمینه یک راهحل جزئی ارائه دادند، اما با محدودیت بزرگ خودشان همراه بودند: ارتباط ناکارآمد دادهها.
اینجاست که SharedArrayBuffer
(SAB) وارد میشود؛ یک ویژگی قدرتمند که با معرفی اشتراکگذاری حافظه واقعی و سطح پایین بین ریسمانها در وب، اساساً قواعد بازی را تغییر میدهد. SAB در کنار شیء Atomics
، عصر جدیدی از برنامههای کاربردی با کارایی بالا و همزمان را مستقیماً در مرورگر باز میکند. با این حال، با قدرت زیاد، مسئولیت—و پیچیدگی—زیادی نیز به همراه میآید.
این راهنما شما را به سفری عمیق در دنیای برنامهنویسی همزمان در جاوااسکریپت خواهد برد. ما بررسی خواهیم کرد که چرا به آن نیاز داریم، SharedArrayBuffer
و Atomics
چگونه کار میکنند، ملاحظات امنیتی حیاتی که باید به آنها توجه کنید، و مثالهای عملی برای شروع.
دنیای قدیم: مدل تکریسمانی جاوااسکریپت و محدودیتهای آن
قبل از اینکه بتوانیم راهحل را درک کنیم، باید مشکل را به طور کامل بفهمیم. اجرای جاوااسکریپت در یک مرورگر به طور سنتی روی یک ریسمان واحد، که اغلب "ریسمان اصلی" یا "ریسمان UI" نامیده میشود، اتفاق میافتد.
حلقه رویداد (The Event Loop)
ریسمان اصلی مسئول همهچیز است: اجرای کد جاوااسکریپت شما، رندر کردن صفحه، پاسخ به تعاملات کاربر (مانند کلیک و اسکرول) و اجرای انیمیشنهای CSS. این ریسمان وظایف را با استفاده از یک حلقه رویداد مدیریت میکند که به طور مداوم صفی از پیامها (وظایف) را پردازش میکند. اگر یک وظیفه زمان زیادی برای تکمیل شدن نیاز داشته باشد، کل صف را مسدود میکند. هیچ کار دیگری نمیتواند انجام شود—رابط کاربری قفل میشود، انیمیشنها دچار لکنت میشوند و صفحه غیرپاسخگو میشود.
Web Workers: گامی در مسیر درست
Web Workers برای کاهش این مشکل معرفی شدند. یک Web Worker اساساً یک اسکریپت است که روی یک ریسمان پسزمینه جداگانه اجرا میشود. شما میتوانید محاسبات سنگین را به یک worker منتقل کنید و ریسمان اصلی را برای مدیریت رابط کاربری آزاد نگه دارید.
ارتباط بین ریسمان اصلی و یک worker از طریق API به نام postMessage()
انجام میشود. وقتی دادهای را ارسال میکنید، توسط الگوریتم کلون ساختاریافته (structured clone algorithm) مدیریت میشود. این به این معنی است که دادهها سریالسازی، کپی و سپس در زمینه worker دسریالسازی میشوند. با اینکه این روش مؤثر است، اما برای مجموعهدادههای بزرگ معایب قابل توجهی دارد:
- سربار عملکردی: کپی کردن مگابایتها یا حتی گیگابایتها داده بین ریسمانها کند و نیازمند پردازش CPU است.
- مصرف حافظه: این کار یک کپی از دادهها در حافظه ایجاد میکند که میتواند برای دستگاههایی با حافظه محدود یک مشکل بزرگ باشد.
یک ویرایشگر ویدیو در مرورگر را تصور کنید. ارسال یک فریم کامل ویدیو (که میتواند چندین مگابایت باشد) به یک worker و بازگرداندن آن برای پردازش ۶۰ بار در ثانیه، هزینهای سرسامآور خواهد داشت. این دقیقاً همان مشکلی است که SharedArrayBuffer
برای حل آن طراحی شده است.
تغییردهنده بازی: معرفی SharedArrayBuffer
یک SharedArrayBuffer
یک بافر داده باینری خام با طول ثابت است، شبیه به یک ArrayBuffer
. تفاوت حیاتی این است که یک SharedArrayBuffer
میتواند بین چندین ریسمان (مانند ریسمان اصلی و یک یا چند Web Worker) به اشتراک گذاشته شود. وقتی شما یک SharedArrayBuffer
را با استفاده از postMessage()
"ارسال" میکنید، شما یک کپی ارسال نمیکنید؛ شما یک مرجع به همان بلوک از حافظه را ارسال میکنید.
این بدان معناست که هر تغییری که توسط یک ریسمان در دادههای بافر ایجاد شود، فوراً برای تمام ریسمانهای دیگری که به آن مرجع دارند، قابل مشاهده است. این کار مرحله پرهزینه کپی و سریالسازی را حذف میکند و اشتراکگذاری داده تقریباً آنی را ممکن میسازد.
اینگونه به آن فکر کنید:
- Web Workers با
postMessage()
: این مانند دو همکاری است که روی یک سند کار میکنند و نسخههای آن را برای یکدیگر ایمیل میکنند. هر تغییر نیازمند ارسال یک نسخه کاملاً جدید است. - Web Workers با
SharedArrayBuffer
: این مانند دو همکاری است که روی یک سند مشترک در یک ویرایشگر آنلاین (مانند Google Docs) کار میکنند. تغییرات برای هر دو به صورت لحظهای قابل مشاهده است.
خطر حافظه مشترک: شرایط رقابتی (Race Conditions)
اشتراکگذاری آنی حافظه قدرتمند است، اما یک مشکل کلاسیک از دنیای برنامهنویسی همزمان را نیز معرفی میکند: شرایط رقابتی.
یک شرایط رقابتی زمانی رخ میدهد که چندین ریسمان سعی میکنند به طور همزمان به یک داده مشترک دسترسی پیدا کرده و آن را تغییر دهند، و نتیجه نهایی به ترتیب غیرقابل پیشبینی اجرای آنها بستگی دارد. یک شمارنده ساده که در یک SharedArrayBuffer
ذخیره شده را در نظر بگیرید. هم ریسمان اصلی و هم یک worker میخواهند آن را افزایش دهند.
- ریسمان A مقدار فعلی را که ۵ است، میخواند.
- قبل از اینکه ریسمان A بتواند مقدار جدید را بنویسد، سیستم عامل آن را متوقف کرده و به ریسمان B سوئیچ میکند.
- ریسمان B مقدار فعلی را که هنوز ۵ است، میخواند.
- ریسمان B مقدار جدید (۶) را محاسبه کرده و آن را در حافظه مینویسد.
- سیستم به ریسمان A بازمیگردد. او نمیداند که ریسمان B کاری انجام داده است. از همان جایی که متوقف شده بود، ادامه میدهد، مقدار جدید خود (۵ + ۱ = ۶) را محاسبه کرده و ۶ را در حافظه مینویسد.
با وجود اینکه شمارنده دو بار افزایش یافت، مقدار نهایی ۶ است، نه ۷. عملیاتها اتمیک نبودند—آنها قابل прерывание بودند و منجر به از دست رفتن دادهها شدند. این دقیقاً دلیلی است که شما نمیتوانید از SharedArrayBuffer
بدون شریک حیاتی آن، یعنی شیء Atomics
، استفاده کنید.
نگهبان حافظه مشترک: شیء Atomics
شیء Atomics
مجموعهای از متدهای استاتیک برای انجام عملیات اتمیک بر روی اشیاء SharedArrayBuffer
فراهم میکند. یک عملیات اتمیک تضمین میشود که به طور کامل و بدون прерывание توسط هیچ عملیات دیگری انجام شود. یا به طور کامل اتفاق میافتد یا اصلاً اتفاق نمیافتد.
استفاده از Atomics
با تضمین اینکه عملیاتهای خواندن-تغییر-نوشتن بر روی حافظه مشترک به صورت ایمن انجام میشوند، از شرایط رقابتی جلوگیری میکند.
متدهای کلیدی Atomics
بیایید به برخی از مهمترین متدهای ارائه شده توسط Atomics
نگاهی بیندازیم.
Atomics.load(typedArray, index)
: به صورت اتمیک مقدار را در یک ایندکس مشخص میخواند و آن را برمیگرداند. این تضمین میکند که شما یک مقدار کامل و خرابنشده را میخوانید.Atomics.store(typedArray, index, value)
: به صورت اتمیک یک مقدار را در یک ایندکس مشخص ذخیره میکند و آن مقدار را برمیگرداند. این تضمین میکند که عملیات نوشتن прерывание نمیشود.Atomics.add(typedArray, index, value)
: به صورت اتمیک یک مقدار را به مقدار موجود در ایندکس مشخص اضافه میکند. این متد مقدار اصلی در آن موقعیت را برمیگرداند. این معادل اتمیکx += value
است.Atomics.sub(typedArray, index, value)
: به صورت اتمیک یک مقدار را از مقدار موجود در ایندکس مشخص کم میکند.Atomics.compareExchange(typedArray, index, expectedValue, replacementValue)
: این یک نوشتن شرطی قدرتمند است. بررسی میکند که آیا مقدار درindex
باexpectedValue
برابر است یا خیر. اگر چنین بود، آن را باreplacementValue
جایگزین میکند وexpectedValue
اصلی را برمیگرداند. در غیر این صورت، هیچ کاری انجام نمیدهد و مقدار فعلی را برمیگرداند. این یک بلوک ساختمانی اساسی برای پیادهسازی مکانیزمهای همگامسازی پیچیدهتر مانند قفلها است.
همگامسازی: فراتر از عملیات ساده
گاهی اوقات شما به چیزی بیشتر از خواندن و نوشتن ایمن نیاز دارید. شما نیاز دارید که ریسمانها هماهنگ شوند و منتظر یکدیگر بمانند. یک ضدالگوی رایج "انتظار مشغول" (busy-waiting) است، جایی که یک ریسمان در یک حلقه فشرده مینشیند و به طور مداوم یک مکان حافظه را برای تغییر بررسی میکند. این کار چرخههای CPU را هدر میدهد و عمر باتری را کاهش میدهد.
Atomics
یک راهحل بسیار کارآمدتر با wait()
و notify()
ارائه میدهد.
Atomics.wait(typedArray, index, value, timeout)
: این به یک ریسمان میگوید که به خواب برود. بررسی میکند که آیا مقدار درindex
هنوزvalue
است. اگر چنین باشد، ریسمان میخوابد تا زمانی که توسطAtomics.notify()
بیدار شود یا تا زمانی کهtimeout
اختیاری (بر حسب میلیثانیه) به پایان برسد. اگر مقدار درindex
قبلاً تغییر کرده باشد، فوراً برمیگردد. این فوقالعاده کارآمد است زیرا یک ریسمان در حال خواب تقریباً هیچ منبع CPU مصرف نمیکند.Atomics.notify(typedArray, index, count)
: این برای بیدار کردن ریسمانهایی استفاده میشود که از طریقAtomics.wait()
در یک مکان حافظه خاص خوابیدهاند. این حداکثرcount
ریسمان منتظر را بیدار میکند (یا همه آنها را اگرcount
ارائه نشده باشد یاInfinity
باشد).
کنار هم قرار دادن همه چیز: یک راهنمای عملی
حالا که تئوری را فهمیدیم، بیایید مراحل پیادهسازی یک راهحل با استفاده از SharedArrayBuffer
را طی کنیم.
مرحله ۱: پیشنیاز امنیتی - ایزولهسازی میانمبداء (Cross-Origin Isolation)
این رایجترین مانع برای توسعهدهندگان است. به دلایل امنیتی، SharedArrayBuffer
فقط در صفحاتی در دسترس است که در حالت ایزوله میانمبداء (cross-origin isolated) هستند. این یک اقدام امنیتی برای کاهش آسیبپذیریهای اجرای سوداگرانه مانند Spectre است که به طور بالقوه میتوانند از تایمرهای با وضوح بالا (که توسط حافظه مشترک ممکن شدهاند) برای نشت دادهها در بین مبداها استفاده کنند.
برای فعال کردن ایزولهسازی میانمبداء، باید وب سرور خود را طوری پیکربندی کنید که دو هدر HTTP خاص را برای سند اصلی شما ارسال کند:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
Cross-Origin-Opener-Policy: same-origin
(COOP): زمینه مرور سند شما را از سایر اسناد جدا میکند و مانع از تعامل مستقیم آنها با شیء window شما میشود.Cross-Origin-Embedder-Policy: require-corp
(COEP): نیازمند این است که تمام منابع فرعی (مانند تصاویر، اسکریپتها و iframeها) که توسط صفحه شما بارگذاری میشوند، یا از همان مبدا باشند یا به صراحت با هدرCross-Origin-Resource-Policy
یا CORS به عنوان قابل بارگذاری میانمبداء علامتگذاری شده باشند.
راهاندازی این مورد میتواند چالشبرانگیز باشد، به خصوص اگر به اسکریپتها یا منابع شخص ثالثی که هدرهای لازم را ارائه نمیدهند، وابسته باشید. پس از پیکربندی سرور خود، میتوانید با بررسی ویژگی self.crossOriginIsolated
در کنسول مرورگر، بررسی کنید که آیا صفحه شما ایزوله شده است یا خیر. این مقدار باید true
باشد.
مرحله ۲: ایجاد و اشتراکگذاری بافر
در اسکریپت اصلی خود، SharedArrayBuffer
و یک "نما" (view) بر روی آن را با استفاده از یک TypedArray
مانند Int32Array
ایجاد میکنید.
main.js:
// ابتدا ایزولهسازی میانمبداء را بررسی کنید!
if (!self.crossOriginIsolated) {
console.error("این صفحه ایزولهسازی میانمبداء ندارد. SharedArrayBuffer در دسترس نخواهد بود.");
} else {
// یک بافر اشتراکی برای یک عدد صحیح ۳۲ بیتی ایجاد کنید.
const buffer = new SharedArrayBuffer(4);
// یک نما بر روی بافر ایجاد کنید. تمام عملیات اتمیک بر روی نما انجام میشود.
const int32Array = new Int32Array(buffer);
// مقدار را در ایندکس ۰ مقداردهی اولیه کنید.
int32Array[0] = 0;
// یک worker جدید ایجاد کنید.
const worker = new Worker('worker.js');
// بافر اشتراکی (SHARED) را به worker ارسال کنید. این یک انتقال مرجع است، نه یک کپی.
worker.postMessage({ buffer });
// به پیامهای worker گوش دهید.
worker.onmessage = (event) => {
console.log(`Worker اتمام کار را گزارش داد. مقدار نهایی: ${Atomics.load(int32Array, 0)}`);
};
}
مرحله ۳: انجام عملیات اتمیک در Worker
worker بافر را دریافت میکند و اکنون میتواند عملیات اتمیک را بر روی آن انجام دهد.
worker.js:
self.onmessage = (event) => {
const { buffer } = event.data;
const int32Array = new Int32Array(buffer);
console.log("Worker بافر اشتراکی را دریافت کرد.");
// بیایید چند عملیات اتمیک انجام دهیم.
for (let i = 0; i < 1000000; i++) {
// به صورت ایمن مقدار اشتراکی را افزایش دهید.
Atomics.add(int32Array, 0, 1);
}
console.log("Worker کار افزایش را تمام کرد.");
// به ریسمان اصلی اطلاع دهید که کار ما تمام شده است.
self.postMessage({ done: true });
};
مرحله ۴: یک مثال پیشرفتهتر - جمع موازی با همگامسازی
بیایید یک مشکل واقعیتر را حل کنیم: جمع کردن یک آرایه بسیار بزرگ از اعداد با استفاده از چندین worker. ما از Atomics.wait()
و Atomics.notify()
برای همگامسازی کارآمد استفاده خواهیم کرد.
بافر اشتراکی ما سه بخش خواهد داشت:
- ایندکس ۰: یک پرچم وضعیت (۰ = در حال پردازش، ۱ = تکمیل شده).
- ایندکس ۱: یک شمارنده برای تعداد workerهایی که کارشان تمام شده است.
- ایندکس ۲: جمع نهایی.
main.js:
if (self.crossOriginIsolated) {
const NUM_WORKERS = 4;
const DATA_SIZE = 10_000_000;
// [status, workers_finished, result]
// ما از دو عدد صحیح ۳۲ بیتی برای نتیجه استفاده میکنیم تا از سرریز برای جمعهای بزرگ جلوگیری کنیم.
const sharedBuffer = new SharedArrayBuffer(4 * 4); // ۴ عدد صحیح
const sharedArray = new Int32Array(sharedBuffer);
// مقداری داده تصادفی برای پردازش ایجاد کنید
const data = new Uint8Array(DATA_SIZE);
for (let i = 0; i < DATA_SIZE; i++) {
data[i] = Math.floor(Math.random() * 10);
}
const chunkSize = Math.ceil(DATA_SIZE / NUM_WORKERS);
for (let i = 0; i < NUM_WORKERS; i++) {
const worker = new Worker('sum_worker.js');
const start = i * chunkSize;
const end = Math.min(start + chunkSize, DATA_SIZE);
// یک نمای غیراشتراکی برای بخش داده worker ایجاد کنید
const dataChunk = data.subarray(start, end);
worker.postMessage({
sharedBuffer,
dataChunk // این کپی میشود
});
}
console.log('ریسمان اصلی اکنون منتظر اتمام کار workerها است...');
// منتظر بمانید تا پرچم وضعیت در ایندکس ۰ به ۱ تبدیل شود
// این خیلی بهتر از یک حلقه while است!
Atomics.wait(sharedArray, 0, 0); // اگر sharedArray[0] برابر با ۰ باشد، منتظر بمان
console.log('ریسمان اصلی بیدار شد!');
const finalSum = Atomics.load(sharedArray, 2);
console.log(`جمع موازی نهایی: ${finalSum}`);
} else {
console.error('صفحه ایزولهسازی میانمبداء ندارد.');
}
sum_worker.js:
self.onmessage = ({ data }) => {
const { sharedBuffer, dataChunk } = data;
const sharedArray = new Int32Array(sharedBuffer);
// جمع را برای بخش این worker محاسبه کنید
let localSum = 0;
for (let i = 0; i < dataChunk.length; i++) {
localSum += dataChunk[i];
}
// به صورت اتمیک جمع محلی را به کل اشتراکی اضافه کنید
Atomics.add(sharedArray, 2, localSum);
// به صورت اتمیک شمارنده 'workerهای تمام شده' را افزایش دهید
const finishedCount = Atomics.add(sharedArray, 1, 1) + 1;
// اگر این آخرین workerای است که کارش تمام میشود...
const NUM_WORKERS = 4; // در یک برنامه واقعی باید این مقدار پاس داده شود
if (finishedCount === NUM_WORKERS) {
console.log('آخرین worker کارش تمام شد. در حال اطلاعرسانی به ریسمان اصلی.');
// ۱. پرچم وضعیت را به ۱ (تکمیل شده) تغییر دهید
Atomics.store(sharedArray, 0, 1);
// ۲. به ریسمان اصلی که منتظر ایندکس ۰ است، اطلاع دهید
Atomics.notify(sharedArray, 0, 1);
}
};
موارد استفاده و کاربردهای دنیای واقعی
این فناوری قدرتمند اما پیچیده در کجا واقعاً تفاوت ایجاد میکند؟ این فناوری در برنامههایی که نیاز به محاسبات سنگین و قابل موازیسازی بر روی مجموعهدادههای بزرگ دارند، برتری دارد.
- WebAssembly (Wasm): این بهترین مورد استفاده است. زبانهایی مانند C++، Rust و Go پشتیبانی کاملی از چندریسمانی دارند. Wasm به توسعهدهندگان اجازه میدهد تا این برنامههای موجود با کارایی بالا و چندریسمانی (مانند موتورهای بازی، نرمافزارهای CAD و مدلهای علمی) را برای اجرا در مرورگر کامپایل کنند و از
SharedArrayBuffer
به عنوان مکانیزم زیربنایی برای ارتباط بین ریسمانها استفاده کنند. - پردازش داده در مرورگر: مصورسازی داده در مقیاس بزرگ، استنتاج مدلهای یادگیری ماشین در سمت کلاینت و شبیهسازیهای علمی که مقادیر عظیمی از داده را پردازش میکنند، میتوانند به طور قابل توجهی تسریع شوند.
- ویرایش رسانه: اعمال فیلترها بر روی تصاویر با وضوح بالا یا انجام پردازش صوتی بر روی یک فایل صوتی را میتوان به بخشهای کوچکتر تقسیم کرد و به صورت موازی توسط چندین worker پردازش کرد، که بازخورد لحظهای را به کاربر ارائه میدهد.
- بازیهای با کارایی بالا: موتورهای بازی مدرن به شدت به چندریسمانی برای فیزیک، هوش مصنوعی و بارگذاری داراییها متکی هستند.
SharedArrayBuffer
امکان ساخت بازیهای با کیفیت کنسول را که کاملاً در مرورگر اجرا میشوند، فراهم میکند.
چالشها و ملاحظات نهایی
در حالی که SharedArrayBuffer
تحولآفرین است، اما یک راهحل جادویی نیست. این یک ابزار سطح پایین است که نیاز به مدیریت دقیق دارد.
- پیچیدگی: برنامهنویسی همزمان به طور بدنامی دشوار است. اشکالزدایی شرایط رقابتی و بنبستها (deadlocks) میتواند فوقالعاده چالشبرانگیز باشد. شما باید به طور متفاوتی در مورد نحوه مدیریت وضعیت برنامه خود فکر کنید.
- بنبستها (Deadlocks): یک بنبست زمانی رخ میدهد که دو یا چند ریسمان برای همیشه مسدود میشوند و هر کدام منتظر دیگری برای آزاد کردن یک منبع هستند. این اتفاق میتواند در صورت پیادهسازی نادرست مکانیزمهای قفلگذاری پیچیده رخ دهد.
- سربار امنیتی: الزام ایزولهسازی میانمبداء یک مانع قابل توجه است. این میتواند ادغام با سرویسهای شخص ثالث، تبلیغات و درگاههای پرداخت را در صورتی که از هدرهای CORS/CORP لازم پشتیبانی نکنند، مختل کند.
- برای هر مشکلی مناسب نیست: برای وظایف پسزمینه ساده یا عملیات I/O، مدل سنتی Web Worker با
postMessage()
اغلب سادهتر و کافی است. فقط زمانی به سراغSharedArrayBuffer
بروید که یک گلوگاه واضح و وابسته به CPU دارید که شامل مقادیر زیادی داده است.
نتیجهگیری
SharedArrayBuffer
، در کنار Atomics
و Web Workers، یک تغییر پارادایم برای توسعه وب محسوب میشود. این فناوری مرزهای مدل تکریسمانی را در هم میشکند و دستهای جدید از برنامههای قدرتمند، با کارایی بالا و پیچیده را به مرورگر دعوت میکند. این پلتفرم وب را برای وظایف محاسباتی سنگین در جایگاهی برابرتر با توسعه برنامههای بومی قرار میدهد.
سفر به جاوااسکریپت همزمان چالشبرانگیز است و نیازمند یک رویکرد دقیق به مدیریت وضعیت، همگامسازی و امنیت است. اما برای توسعهدهندگانی که به دنبال پیش بردن مرزهای ممکن در وب هستند—از سنتز صوتی لحظهای گرفته تا رندرینگ سهبعدی پیچیده و محاسبات علمی—تسلط بر SharedArrayBuffer
دیگر فقط یک گزینه نیست؛ بلکه یک مهارت ضروری برای ساخت نسل بعدی برنامههای وب است.